package top.hmtools.wxmp.accessToken;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import top.hmtools.wxmp.UrlServer.EUrlServer;
import top.hmtools.wxmp.httpclient.HttpClientHandleTools;

/**
 * 微信access Token 工具
 * @author Hybomyth
 *
 */
public class AccessTokenTools {
	
	Logger logger = LoggerFactory.getLogger(AccessTokenTools.class);
	
	private static AccessTokenTools accessTokenTools;

	/**
	 * 存储appid，secret，access Token信息的实体类
	 */
	private volatile  ATBean accessTokenBean;
	
	/**
	 * 序列化时，存放在磁盘上的路径
	 */
	private String atBeanSerializablePath = System.getProperty("user.dir")+File.separator+"atBean.bean";
	
	private String get_token_uri = "cgi-bin/token";
	
	/**
	 * 请求的微信服务器地址
	 */
	private static String apiServerUrl;

	private String appid;

	private String appsecret;
	
	private AccessTokenTools(){
		
	}
	
//	private AccessTokenTools(EUrlServer eUrlServer) {
//		this.apiServerUrl = "https://"+eUrlServer.toString()+"/";
//	}
	
	/**
	 * 获取access_token实体对象
	 * <p>
	 * 由于微信侧提供的access Token 有有效时间及每日获取次数限制，为了充分使用资源，因此本方法会尝试先从本地获取序列化后的access Token，当本地获取异常时，才从微信侧获取access Token，同时也会序列化数据到本地。
	 * </p>
	 * <br>获取access token思路流程：
	 * <br>1、先检查当前运行时内存中是否有 accessToken
	 * <br>2、内存中没有，则尝试从本地磁盘缓存的序列化文件中读取
	 * <br>3、本地磁盘中也没有则从微信服务器处获取新的，同时并缓存的本地内存、磁盘中
	 * @param appid
	 * @param appsecret
	 * @return
	 */
	public ATBean getAccessTokenInfoBean(String appid,String appsecret){
		if(appid==null || appid.length()<1 || appsecret ==null || appsecret.length()<1){
			throw new IllegalArgumentException("入参为空");
		}
		this.appid=appid;
		this.appsecret=appsecret;
		//1、先检查当前运行时内存中是否有 accessToken
		if(this.accessTokenBean == null){
			//accessTokenBean 为单例，需要考虑线程安全问题
			synchronized (this) {
				//先尝试从磁盘中获取序列化的atbean
				ObjectInputStream ois = null;
				try {
					//2、内存中没有，则尝试从本地磁盘缓存的序列化文件中读取
					File file = new File(atBeanSerializablePath);
					if(file.exists()){
						this.logger.debug("access token序列化文件存放路径：{}",file.getAbsoluteFile());
						ois = new ObjectInputStream(FileUtils.openInputStream(file.getAbsoluteFile()));
						this.accessTokenBean = (ATBean) ois.readObject();
					}
					
					if(this.accessTokenBean !=null){
						//验证accessToken是否过期，如果过期，则需要重新获取
						if(!this.isExpiresIn(this.accessTokenBean)){
							return this.getATbeanFromWX(appid, appsecret);
						}
						//如果获取的序列化对象实例中的 appid 与 APPsecret 不一致，则需要重新获取
						if(!appid.equals(this.accessTokenBean.getAppid()) || !appsecret.equals(this.accessTokenBean.getSecret())){
							return this.getATbeanFromWX(appid, appsecret);
						}
					}else{
						//3、本地磁盘中也没有则从微信服务器处获取新的，同时并缓存的本地内存、磁盘中
						return this.getATbeanFromWX(appid, appsecret);
					}
					
				} catch (Exception e) {
					// skip
					this.logger.error("读取序列化的access Token信息失败："+e.getMessage(),e);
				}finally{
					if(ois!=null){
						try {
							ois.close();
						} catch (IOException e) {
							this.logger.error(e.getMessage(),e);
						}
					}
				}
			}
		}else{
			if(!this.isExpiresIn(this.accessTokenBean)){
				return this.getATbeanFromWX(appid, appsecret);
			}
		}
		return this.accessTokenBean;
	}
	
	/**
	 * 清理本地并重新获取access Token
	 * <br>必须曾先执行过 top.hmtools.wxmp.accessToken.AccessTokenTools.getAccessTokenInfoBean(String, String)
	 * @return
	 */
	public ATBean getRefreshedAccessToken(){
		return this.getRefreshedAccessToken(this.appid, this.appsecret);
	}
	
	/**
	 * 清理本地并重新获取access Token
	 * @param appid
	 * @param appsecret
	 * @return
	 */
	public synchronized ATBean getRefreshedAccessToken(String appid,String appsecret){
		this.logger.info("开始刷新access Token");
		//先删除本地序列化后的access Token
		File file = new File(atBeanSerializablePath);
		if(file.exists()){
			file.delete();
		}
		this.logger.info("执行完删除本地磁盘access Token缓存");
		
		//再清空内存中的
		this.accessTokenBean = null;
		this.logger.info("执行完清空内存中access Token缓存");
		
		//重新获取
		ATBean result = this.getAccessTokenInfoBean(appid, appsecret);
		this.logger.info("执行完重新获取access Token：{}",result);
		return result;
	}
	
	/**
	 * 从微信服务器获取access Token 信息，但不将结果序列化到磁盘
	 * @param appid
	 * @param appsecret
	 * @return
	 */
	public ATBean getATbeanFromWXOnly(String appid,String appsecret){
		this.accessTokenBean = new ATBean();
		
		//从微信服务器获取accessToken
		CloseableHttpClient httpClient = HttpClientHandleTools.getPoolHttpClient();
		HttpGet get = new HttpGet(apiServerUrl+this.get_token_uri+"?grant_type=client_credential&appid="+appid+"&secret="+appsecret);
		try {
			CloseableHttpResponse response = httpClient.execute(get);
			InputStream content = response.getEntity().getContent();
			String contentStr = IOUtils.toString(content, "utf-8");
			JSONObject jsonObject = JSON.parseObject(contentStr);
			String access_token= jsonObject.getString("access_token");
			long expires_in = jsonObject.getLongValue("expires_in");
			
			accessTokenBean.setAccess_token(access_token);
			accessTokenBean.setAppid(appid);
			accessTokenBean.setExpires_in(expires_in);
			accessTokenBean.setLastModifyDatetime(new Date());
			accessTokenBean.setSecret(appsecret);
		} catch (IOException e) {
			this.accessTokenBean = null;
			this.logger.error("从微信服务器获取access Token异常："+e.getMessage(),e);
		}
		return accessTokenBean;
	}

	/**
	 * 获取access token 值
	 * @return
	 */
	public String getAccessToken() {
		if(this.accessTokenBean!=null){
			return this.accessTokenBean.getAccess_token();
		}else{
			return "";
		}
	}
	
	/**
	 * 手动设置accessToken值
	 * @param accessToken
	 */
	@Deprecated
	public void setAccessToken(String accessToken) {
		if(this.accessTokenBean!=null){
			this.accessTokenBean.setAccess_token(accessToken);
		}
	}

	/**
	 * 获取access token实体类对象实例
	 * @return
	 */
	public ATBean getAccessTokenBean() {
		return accessTokenBean;
	}

	/**
	 * 线程安全的手动设置access token实体类对象实例
	 * @param accessTokenBean
	 */
	public synchronized void setAccessTokenBean(ATBean accessTokenBean) {
		this.accessTokenBean = accessTokenBean;
		this.doSerializable(accessTokenBean);
	}
	
	/**
	 * 获取 AccessTokenTools 对象实例
	 * @param eUrlServer
	 * @return
	 */
	public static AccessTokenTools getInstance(EUrlServer eUrlServer){
		if(eUrlServer == null){
			eUrlServer = EUrlServer.api;
		}
		//获取配置的服务器地址
		apiServerUrl = "https://"+eUrlServer.toString()+"/";
		
		//实例化 AccessTokenTools
		if(accessTokenTools == null){
			synchronized (AccessTokenTools.class) {
				if(accessTokenTools == null){
					accessTokenTools = new AccessTokenTools();
				}
			}
		}
		return accessTokenTools;
	}

	/**
	 * 验证是否过期
	 * @return
	 */
	private boolean isExpiresIn(ATBean atBean){
		long expires_in = atBean.getExpires_in();
		Date lastModifyDatetime = atBean.getLastModifyDatetime();
		Date now = new Date();
		if((now.getTime()-lastModifyDatetime.getTime())>expires_in*1000){
			return false;
		}else{
			return true;
		}
	}

	/**
	 * 从微信服务器获取access Token 信息，并将结果序列化到磁盘
	 * @param appid
	 * @param appsecret
	 * @return
	 */
	private ATBean getATbeanFromWX(String appid,String appsecret){
		this.accessTokenBean =this.getATbeanFromWXOnly(appid,appsecret);
		this.doSerializable(accessTokenBean);
		return accessTokenBean;
	}

	/**
	 * 执行序列化
	 * @param atBean
	 */
	private void doSerializable(ATBean atBean){
		try {
			ObjectOutputStream oos = new ObjectOutputStream(FileUtils.openOutputStream(new File(this.atBeanSerializablePath)));
			oos.writeObject(atBean);
		} catch (IOException e) {
			this.logger.error("序列化access Token 信息失败："+e.getMessage(),e);
		}
	}
}
